#!/usr/bin/python
# -*- coding: UTF-8 -*-
# vim: expandtab sw=4 ts=4 sts=4
#
#     This file is part of IMAPylator
#
#    IMAPylator is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    Foobar is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with Foobar.  If not, see <http://www.gnu.org/licenses/>.


import sys
import imaplib
import time
import os
import optparse, logging
from string import strip, split, join
import email, re

_mods_dir_ = "mods"
_match_class_name_ = "IMAPylatorMatch"
MODULE_EXTENSIONS = ('.py')


class IMAP:
    '''
    Wrapper class for IMAP utilities. It takes care about configuration
    and connecting to IMAP.
    '''
    def __init__(self, host, ssl=False, port=None):
        self._host = host
        self._ssl = ssl
        
        if port:
            self._port = port
        elif ssl == True:
            self._port = 993
        else :
            self._port = 143

    def login(self, user, password, type="LOGIN", authuser=None):
        '''
        Logins to IMAP server according to configuration.
        '''

        # Connect to server
        if self._ssl:
            self._imap = imaplib.IMAP4_SSL(self._host, self._port)
        else:
            self._imap = imaplib.IMAP4(self._host, self._port)

        if type == "LOGIN":
            # Login
            res = self._imap.login(user, password)
            if res[0] != 'OK':
                raise Exception("imapLogin Error")
            return
        if type == "CRAM-MD5":
            res = self._imap.login_cram_md5(user, password)
            if res[0] != 'OK':
                raise Exception("imapLogin Error")
            return
        if type == "PLAIN":
            def plain_callback(response):
                if authuser is None:
                    return "%s\x00%s\x00%s" % (user, user, password)
                else:
                    return "%s\x00%s\x00%s" % (authuser, user, password)
            
            res = self._imap.authenticate('PLAIN', plain_callback)
            if res[0] != 'OK':
                raise Exception("imapLogin Error")
            return

    def fetchHeaders(self, num):
        '''
        Fetch message headers
        '''
        typ, data = self._imap.fetch(num, '(BODY.PEEK[HEADER])')
        if typ != 'OK':
            raise Exception("fetchHeader Error: %d" % num)
            return

        header_dict = {}
        
        for header in data[0][1].split('\n'):
            if len(header) == 0:
                continue
            val = strip(header).split(":", 1)
            if len(val) == 1:
                continue
            
            header_dict[strip(val[0])] = strip(val[1])

        return header_dict

    def fetchRFC822(self, num):
        '''
        Fetch message 
        '''
        typ, data = self._imap.fetch(num, '(RFC822)')
        if typ != 'OK':
            raise Exception("fetchRFC822 Error: %d" % num)
        else:
            return email.message_from_string(data[0][1])
    
    def listMessages(self, folder):
        '''
        Fetches messages from folder.
        '''
        res = self._imap.select(folder)
        if res[0] != 'OK':
            raise Exception("Select Error: %s" % folder)
        else:
            typ, data = self._imap.search(None, 'ALL')   
            if typ != 'OK':
                raise Exception("listMessages Error")
            else:
                return data[0].split()
    
    def selectFolder(self, folder):
        '''
        Examine folder
        '''
        res = self._imap.select(folder)
        if res[0] != 'OK':
            raise Exception("Select Error: %s" % folder)
        else:
            return int(res[1][0])

    def listFolders(self):
        '''
        List all folders
        '''
        res, data = self._imap.list()
        if res != 'OK':
            raise Exception("listFolders Error")
        folders=[]
        for f in data:
            folders.append(f.split('"')[3]) 

        return folders
    
    def getMessageFlags(self, num):
        typ, response = self._imap.fetch(num, '(FLAGS)')
        if typ != 'OK':
            raise Exception("getMessageFlagss Error")
        
        regExFlags = re.compile(r"{\\Answered|\\Flagged|\\Deleted|\\Seen|\\Draft}")
        flags = regExFlags.findall(response[0])
        imapFlags = ""
        for item in flags:
            imapFlags += item + " "
        imapFlags = imapFlags[:-1]
        return imapFlags
    
    def getInternalDate(self, num):
        typ, response = self._imap.fetch(num, '(INTERNALDATE)')
        if typ != 'OK':
            raise Exception("getInternalDate Error")
        #temptup = split(response[0])[2:]
        #tstr = temptup[0][1:] + temptup[1] + temptup[2][:-2]
        #print tstr
        date = response[0].split(' ',1)[1][1:-1]
        return imaplib.Internaldate2tuple(date)

    def uploadMessage(self, folder, message, flags="", date=None):
        typ, response = self._imap.append(folder, flags, date, str(message))
        if typ != 'OK':
            raise Exception("uploadMessage Error")
    
    def deleteMessage(self, num):
        typ, response = self._imap.store(num, 'FLAGS.SILENT', r"(\Deleted)")
        if typ != 'OK':
            raise Exception("deleteMessage Error")

    def expunge(self):
        typ, response = self._imap.expunge()
        if typ != 'OK':
            raise Exception("expunge Error")


def print_abstract(message, stream): 
    stream("Subject: %s" % manipulator.message["Subject"])
    stream("From: %s" % manipulator.message["From"])
    stream("To: %s" % manipulator.message["To"])
    stream("Date: %s" % manipulator.message["Date"])

if __name__ == '__main__':

    class MakeIMAPylatorOptionParser (optparse.OptionParser):
        def check_required (self, opt):
            option = self.get_option(opt)
            if getattr(self.values, option.dest) is None:
                raise Exception ("%s option not supplied" % option)

    parser = MakeIMAPylatorOptionParser("usage: %prog [options]")
    parser.add_option('-u', '--user', type="string", help="Username")
    parser.add_option('-p', '--password', type="string", help="Password")
    parser.add_option('-H', '--host', type="string", help="IMAP Server IP or name")
    parser.add_option('-P', '--port', type="int", help="TCP port")
    parser.add_option('-s', '--ssl', action="store_true", default=False, help="use SSL")
    parser.add_option('-l', '--log', type="string", help="log file")
    parser.add_option('-d', '--debug', action="store_true", default=False, help="enable debugging")
    parser.add_option('-o','--one-shot', action="store_true", default=False, help="one shot mode")
    parser.add_option('-m', '--mod', type="string", help="module to use")
    parser.add_option('-F', '--match-folder-opt', type="string", help="match folder module option")
    parser.add_option('-M', '--match-opt', type="string", help="match module option")
    parser.add_option('-A', '--action-opt', type="string", help="action module option")
    parser.add_option('--lm','--list-modules', action="store_true", default=False, help="list installed modules")
    parser.add_option('--am', '--auth-mechanism', type="choice", choices=("LOGIN", "CRAM-MD5", "PLAIN"), default="LOGIN", help="authentication method")
    parser.add_option('--au', '--auth-user', type="string", help="auth user for PLAIN login")
    
    (options,args)=parser.parse_args()
 
    logger = logging.getLogger(sys.argv[0])
    logger.debug("Start")
     
    if options.log != None:
        hdlr = logging.FileHandler(options.log)
    else:
        hdlr = logging.StreamHandler()

    formatter = logging.Formatter('%(asctime)s %(levelname)s %(message)s')
    hdlr.setFormatter(formatter)
    logger.addHandler(hdlr)
    
    if options.debug:
        logger.setLevel(logging.DEBUG)
    else: 
        logger.setLevel(logging.INFO)
   
    if options.lm == True:
        modules = [os.path.splitext(module)[0] for module in os.listdir(_mods_dir_) if module.endswith(MODULE_EXTENSIONS) and not module.startswith("__") ]
        
        for m in modules:
            try:
                exec("from %s import %s" % (_mods_dir_, m))
                
                docstring = eval("%s.__doc__" % (m))
                print "\n%s:" % m
                print docstring or "Not documented"
                
                print "Match Folder options (-F switch):"
                docstring = eval("%s.%s.matchFolder.__doc__" % (m, m))
                print docstring or "Not documented"
                
                print "Match options (-M switch)"
                docstring = eval("%s.%s.match.__doc__" % (m, m))
                print docstring or "Not documented"
                
                print "Action options (-A switch)"
                docstring = eval("%s.%s.action.__doc__" % (m, m))
                print docstring or "Not documented"

            except Exception, e:
                print "Errore: %s" % e

        sys.exit()

    try:
        parser.check_required('-H')
        parser.check_required('-u')
        parser.check_required('-p')
        parser.check_required('-m')
    except Exception, e:
        logger.error("Error: Host, user, password and module il required.")
        sys.exit(-1)
  
    try:
        # IMAP Login
        f = IMAP(options.host, ssl=options.ssl, port=options.port)
        f.login(options.user, options.password, options.am, options.au)
    except Exception, e:
        logger.error("%s" % e)
        sys.exit(-1)
    
    try:
        exec("from %s import %s" % (_mods_dir_, options.mod))
        manipulator = eval("%s.%s" % (options.mod, options.mod))()
    except Exception, e:
        logger.error("%s" % e)
        sys.exit(-1)
    
    try:
        # folder list
        folders = f.listFolders()
    except Exception, e:
        logger.error("%s" % e)
        sys.exit(-1)
    

    for folder in folders:
        logger.debug("calling %s.matchFolder(%s, %s)" % (options.mod, folder, options.match_folder_opt))
        if manipulator.matchFolder(folder, options.match_folder_opt):
            logger.debug("listing message in %s" % folder)
            try:
                num_messages = f.selectFolder(folder)
            except Exception, e:
                logger.error("%s folder is not selectable: %s. skipping" % (folder, e))
                continue

            logger.debug("found %d messages" % num_messages)
            
            uid = 1

            while uid <= num_messages:
                uidvalidity = True # False if uidvalidity change. See http://www.faqs.org/rfcs/rfc3501.html
                
                logger.debug("fetching message headers %s[%s] " % (folder, uid))
                
                try:
                    headers = f.fetchHeaders(uid)
                except Exception, e:
                    logger.error("%s" % e)
                    sys.exit(-1)

                logger.debug("fetching message flags %s[%s] " % (folder, uid))

                try:
                    flags = f.getMessageFlags(uid)
                except Exception, e:
                    logger.error("%s" % e)
                    sys.exit(-1)
                
                logger.debug("fetching message INTERNALDATE %s[%d] " % (folder, uid))

                try:
                    internal_date = f.getInternalDate(uid)
                except Exception, e:
                    logger.error("%s" % e)
                    sys.exit(-1)

                logger.debug("calling %s.match(headers, flags, internal_date, folder, %s)" % (options.mod, options.match_opt))
                logger.debug("headers:")

                for n,v in headers.items():
                    logger.debug("%s: %s" % (n,v))
                logger.debug("\n")
                
                if manipulator.match(headers, flags, internal_date, folder, options.match_opt):

                    logger.debug("fetching RFC822 message %s[%s] " % (folder, uid))

                    try:
                        message = f.fetchRFC822(uid)
                    except Exception, e:
                        logger.error("%s" % e)
                        sys.exit(-1)
                    
                    logger.debug("calling %s.action(message, flags, folder, internal_date, opt=%s" % (options.mod, options.action_opt))
                    
                    action = manipulator.action(message=f.fetchRFC822(uid), flags=flags, folder=folder, internal_date=internal_date, opt=options.action_opt)
                    
                    if action == "upload":
                        logger.debug("Folder: %s" % manipulator.folder)
                        logger.debug("Flags: %s" % manipulator.flags)
                        print_abstract(manipulator.message, logger.info)    

                        logger.info("uploading message: Folder: %s Flags: %s" % (manipulator.folder, manipulator.flags))

                        try:
                            f.uploadMessage(manipulator.folder, manipulator.message.as_string(False), flags=manipulator.flags, date=manipulator.internal_date)
                        except Exception, e:
                            logger.error("%s" % e)
                            sys.exit(-1)
                        
                        if options.one_shot:
                            sys.exit()
                    
                    elif action == "delete":
                        logger.debug("Folder: %s" % manipulator.folder)
                        logger.debug("Flags: %s" % manipulator.flags)
                        print_abstract(manipulator.message, logger.info)    

                        logger.info("deleting message: Folder: %s Flags: %s" % (manipulator.folder, manipulator.flags))

                        try:
                            f.deleteMessage(uid)
                        except Exception, e:
                            logger.error("%s" % e)
                            sys.exit(-1)
                        
                        try:
                            f.expunge()
                        except Exception, e:
                            logger.error("%s" % e)
                            sys.exit(-1)
                        else:
                            # Message sequence numbers can be reassigned during the session.  For
                            # example, when a message is permanently removed (expunged) from the
                            # mailbox, the message sequence number for all subsequent messages is
                            # decremented.ù
                            logger.debug("resetting IMAP UIDVALIDITY")
                            uidvalidity = False
                    
                        if options.one_shot:
                            sys.exit()

                    elif action == "replace":
                        
                        logger.info("deleting message: Folder: %s Flags: %s" % (manipulator.folder, manipulator.flags))
                        
                        try:
                            f.deleteMessage(uid)
                        except Exception, e:
                            logger.error("%s" % e)
                            sys.exit(-1)
                        
                        logger.info("expunging messaged")
                        
                        try:
                            f.expunge()
                        except Exception, e:
                            logger.error("%s" % e)
                            sys.exit(-1)
                        else:
                            # Message sequence numbers can be reassigned during the session.  For
                            # example, when a message is permanently removed (expunged) from the
                            # mailbox, the message sequence number for all subsequent messages is
                            # decremented.
                            logger.debug("resetting IMAP UIDVALIDITY")
                            uidvalidity = False
 
                        logger.debug("Folder: %s" % manipulator.folder)
                        logger.debug("Flags: %s" % manipulator.flags)
                        print_abstract(manipulator.message, logger.info)    

                        logger.info("uploading message: Folder: %s Flags: %s" % (manipulator.folder, manipulator.flags))
                            
                        try:
                            f.uploadMessage(manipulator.folder, manipulator.message.as_string(False), flags=manipulator.flags, date=manipulator.internal_date)
                        except Exception, e:
                            logger.error("%s" % e)
    
                        if options.one_shot:
                            sys.exit()

                    elif action == "print" or action == "dunno":
                        logger.debug("Folder: %s" % manipulator.folder)
                        logger.debug("Flags: %s" % manipulator.flags)
                        print_abstract(manipulator.message, logger.info)    

                        logger.info("Message: %s\n" % manipulator.message)
                        
                        if options.one_shot:
                            sys.exit()
                    
                    elif action == "skip":
                        logger.debug("Folder: %s" % manipulator.folder)
                        logger.debug("Flags: %s" % manipulator.flags)
                        print_abstract(manipulator.message, logger.debug)    
                        logger.debug("skipping")

                        if options.one_shot:
                            sys.exit()

                    else:
                        logger.error("action invalid")
                    
                if uidvalidity == True:
                    uid = uid + 1
